﻿using System;
using Lypshare.Net;
using System.Timers;
using Lypshare.Flow.Packets;
using Lypshare.Net.Client;

namespace Lypshare.Flow
{
	/// <summary>
	/// Flow client.
	/// </summary>
	/// <exception cref='ArgumentNullException'>
	/// Is thrown when an argument passed to a method is invalid because it is <see langword="null" /> .
	/// </exception>
	/// <exception cref='InvalidOperationException'>
	/// Is thrown when an operation cannot be performed.
	/// </exception>
    public class FlowClient<T> : IFlowClient where T: IFlowControl
    {
        // --------------------------------------------------------
        // Class Events
        // --------------------------------------------------------


        /// <summary>
        /// Occurs when data received.
        /// </summary>
        public event Action<IFlowClient, DataEventArgs> DataReceived = null;


        /// <summary>
        /// Occurs when data sent.
        /// </summary>
        public event Action<IFlowClient, DataEventArgs> DataSent = null;


        /// <summary>
        /// Occurs when speed changed.
        /// </summary>
        public event Action<IFlowClient> SpeedChanged = null;


        /// <summary>
        /// Occurs when synchronized.
        /// </summary>
        public event Action<IFlowClient> Synchronized = null;


        /// <summary>
        /// Occurs when closing.
        /// </summary>
        public event Action<IFlowClient> Closing = null;


        /// <summary>
        /// Occurs when closed.
        /// </summary>
        public event Action<IFlowClient> Closed = null;


        // --------------------------------------------------------
        // Class Variables
        // --------------------------------------------------------


        /// <summary>
        /// The _id.
        /// </summary>
        private int _id = 0;


        /// <summary>
        /// The _endpoint.
        /// </summary>
        private FlowEndPoint _endpoint = null;


        /// <summary>
        /// The _flow control.
        /// </summary>
        private T _flowControl = default(T);


        /// <summary>
        /// The _download speed.
        /// </summary>
        private long _downloadSpeed = 0;


        /// <summary>
        /// The _upload speed.
        /// </summary>
        private long _uploadSpeed = 0;


        /// <summary>
        /// The _bytes sent.
        /// </summary>
        private long _bytesSent = 0;


        /// <summary>
        /// The _bytes received.
        /// </summary>
        private long _bytesReceived = 0;


        /// <summary>
        /// The _speed timeout.
        /// </summary>
        private Timer _speedTimeout = null;


        /// <summary>
        /// The _conn timeout.
        /// </summary>
        private Timer _connTimeout = null;


        /// <summary>
        /// The _state.
        /// </summary>
        private FlowClientState _state 
            = FlowClientState.Ready;


        // --------------------------------------------------------
        // Class Properties
        // --------------------------------------------------------


        /// <summary>
        /// Gets the identifier.
        /// </summary>
        /// <value>
        /// The identifier.
        /// </value>
        public int Id 
        {
            get { return _id;  }

        } // Property: Id


        /// <summary>
        /// Gets the endpoint.
        /// </summary>
        /// <value>
        /// The endpoint.
        /// </value>
        public FlowEndPoint Endpoint
        {
            get { return _endpoint; }

        } // Property: Endpoint


        /// <summary>
        /// Gets the state.
        /// </summary>
        /// <value>
        /// The state.
        /// </value>
        public FlowClientState State
        {
            get { return _state; }
        
        } // Property: State


        /// <summary>
        /// Gets the download speed.
        /// </summary>
        /// <value>
        /// The download speed.
        /// </value>
        public long DownloadSpeed 
        {
            get { return _downloadSpeed; }
        
        } // Property: DownloadSpeed


        /// <summary>
        /// Gets the upload speed.
        /// </summary>
        /// <value>
        /// The upload speed.
        /// </value>
        public long UploadSpeed 
        {
            get { return _uploadSpeed; }
        
        } // Property: UploadSpeed


        // --------------------------------------------------------
        // Class Functions
        // --------------------------------------------------------


        /// <summary>
        /// Initializes a new instance of the <see cref="Lypshare.Flow.FlowClient`1"/> class.
        /// </summary>
        /// <param name='id'>
        /// Identifier.
        /// </param>
        public FlowClient(int id)
        {
            _id = id;

            // Initialize the connection timeout
            _connTimeout = new Timer(10000);

            _connTimeout.Elapsed +=
                (s, e) => Dispose();

            // Initialize the speed timeout
            _speedTimeout = new Timer(1000);
            _speedTimeout.AutoReset = true;

            // Link elapsed event
            _speedTimeout.Elapsed 
                += OnSpeedTimerElapsed;

        } // Constructor


        /// <summary>
        /// Connect the specified endpoint.
        /// </summary>
        /// <param name='endpoint'>
        /// Endpoint.
        /// </param>
        /// <exception cref='ArgumentNullException'>
        /// Is thrown when an argument passed to a method is invalid because it is <see langword="null" /> .
        /// </exception>
        /// <exception cref='InvalidOperationException'>
        /// Is thrown when an operation cannot be performed.
        /// </exception>
        public void Connect(FlowEndPoint endpoint)
        {
            if (endpoint == null)
                throw new ArgumentNullException("endpoint");

            if (!endpoint.Connection.IsConnected)
                throw new InvalidOperationException("Endpoint is not connected.");

            if (State != FlowClientState.Ready)
                throw new InvalidOperationException("Flow endpoint is not ready.");

            _endpoint = endpoint;

            _flowControl = (T)Activator.CreateInstance(
                typeof(T), Endpoint);

            // Start speed timer
            _speedTimeout.Start();
			
            // Send sync packet
            Send(new SyncPacket(Id));
			
			_state = FlowClientState.Synchronize;
			
			Endpoint.Connection.DataReceived 
                += OnConnectionDataReceived;

            Endpoint.Connection.Closed 
                += OnConnectionClosed;
			
        } // Connect


        /// <summary>
        /// Send the specified packet.
        /// </summary>
        /// <param name='packet'>
        /// Packet.
        /// </param>
        /// <exception cref='InvalidOperationException'>
        /// Is thrown when an operation cannot be performed.
        /// </exception>
        public void Send(Packet packet)
        {
            if (State == FlowClientState.Synchronize)
                throw new InvalidOperationException(
                    "Flow endpoint is not connected.");

            if (State == FlowClientState.Closing)
                throw new InvalidOperationException(
                    "Flow is already closing.");

            if (State == FlowClientState.Closed)
                throw new InvalidOperationException(
                    "Flow is already closed.");

            // Start connection timeout
            if (!_flowControl.IsEmpty)
                _connTimeout.Start();

            _flowControl.Send(packet);
            
        } // Send


        /// <summary>
        /// Accept the specified packet.
        /// </summary>
        /// <param name='packet'>
        /// If set to <c>true</c> packet.
        /// </param>
        private bool Accept(FlowPacket packet)
        {
            return _flowControl.Accept(packet);

        } // Accept


        /// <summary>
        /// Close this instance.
        /// </summary>
        public virtual void Close()
        {
            // Send close packet if needed
			if(State == FlowClientState.Connected)
            	Send(new ClosePacket());

            // Set state to closing
            _state = FlowClientState.Closing;

            // Raise event
            OnClosing();

        } // Close


        /// <summary>
        /// Releases all resource used by the <see cref="Lypshare.Flow.FlowClient`1"/> object.
        /// </summary>
        /// <remarks>
        /// Call <see cref="Dispose"/> when you are finished using the <see cref="Lypshare.Flow.FlowClient`1"/>.
        /// The <see cref="Dispose"/> method leaves the <see cref="Lypshare.Flow.FlowClient`1"/> in an unusable
        /// state. After calling <see cref="Dispose"/>, you must release all references to the
        /// <see cref="Lypshare.Flow.FlowClient`1"/> so the garbage collector can reclaim the memory that the
        /// <see cref="Lypshare.Flow.FlowClient`1"/> was occupying.
        /// </remarks>
        private void Dispose()
        {
            _state = FlowClientState.Closed;

            _speedTimeout.Stop();
            _connTimeout.Stop();

            _flowControl.Dispose();

            Endpoint.Connection.DataReceived 
                -= OnConnectionDataReceived;

            Endpoint.Connection.Closed 
                -= OnConnectionClosed;

            _endpoint = null;

            OnClosed();

        } // Dispose


        // --------------------------------------------------------
        // Class Handles
        // --------------------------------------------------------


        /// <summary>
        /// Raises the connection data received event.
        /// </summary>
        /// <param name='s'>
        /// S.
        /// </param>
        /// <param name='e'>
        /// E.
        /// </param>
        private void OnConnectionDataReceived(IConnection s, DataEventArgs e)
        {
            if(e.Packet is FlowPacket)
            {
                var flowPacket = e.Packet as FlowPacket;
				
                if (flowPacket.ReceiverId == Id && Accept(flowPacket))
                {
                    if (State == FlowClientState.Synchronize
                        && e.Packet.Content is SyncPacket)
                    {
                        var syncPacket = flowPacket.Content 
                            as SyncPacket;

                        Endpoint.Id = syncPacket.SenderId;
                        _state = FlowClientState.Connected;

                        OnSynchronized();

                    } // if (State == FlowState.Syncronize...
                    else if (State == FlowClientState.Connected
                        && e.Packet.Content is ClosePacket)
                    {
                        Dispose();

                    } // else if (State == FlowState.Connected...
                    else
                    {
                        var content = flowPacket.Content;

                        // Set bytes received
                        _bytesReceived += content.Length;

                        OnDataReceived(new DataEventArgs(
                            flowPacket.Content));

                    } // else
                } // if (flowPacket.ReceiverId == Id && Accept(flowPacket))
            } // if(e.Packet is FlowPacket)
            else if (e.Packet is AckPacket)
            {
                var ackPacket = e.Packet as AckPacket;

                var packets = _flowControl
                    .Acknowledge(ackPacket);

                foreach (var packet in packets)
                {
                    // set bytes sent
                    _bytesSent += packet.Length;
					
                    OnDataSent(new DataEventArgs(
                        packet.Content));

                } // foreach (var packet in packets)

                if(_flowControl.IsEmpty)
                {
                    if (State == FlowClientState.Closing)
                    {
                        Dispose();

                    } // if (State == FlowClientState.Closing)
                    else
                    {
                        _connTimeout.Stop();

                    } // else
                } // if(_flowControl.IsEmpty)
                else
                {
                    // Reset the connection timeout
                    _connTimeout.Stop();
                    _connTimeout.Start();

                } // else
            } // else if (e.Packet is AckPacket)
        } // OnConnectionDataReceived


        /// <summary>
        /// Raises the connection closed event.
        /// </summary>
        /// <param name='s'>
        /// S.
        /// </param>
        private void OnConnectionClosed(IConnection s)
        {
            Dispose();

        } // ConnectionClosed


        /// <summary>
        /// Raises the speed timer elapsed event.
        /// </summary>
        /// <param name='sender'>
        /// Sender.
        /// </param>
        /// <param name='e'>
        /// E.
        /// </param>
        private void OnSpeedTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            _downloadSpeed = _bytesReceived;
            _uploadSpeed = _bytesSent;

            _bytesReceived = 0;
            _bytesSent = 0;

            OnSpeedChanged();

        } // OnSpeedTimerElapsed


        /// <summary>
        /// Raises the data sent event.
        /// </summary>
        /// <param name='e'>
        /// E.
        /// </param>
        private void OnDataSent(DataEventArgs e)
        {
            if (DataSent != null)
                DataSent(this, e);

        } // OnDataSent


        /// <summary>
        /// Raises the data received event.
        /// </summary>
        /// <param name='e'>
        /// E.
        /// </param>
        private void OnDataReceived(DataEventArgs e)
        {
            if (DataReceived != null)
                DataReceived(this, e);

        } // OnDataReceived


        /// <summary>
        /// Raises the speed changed event.
        /// </summary>
        private void OnSpeedChanged()
        {
            if (SpeedChanged != null)
                SpeedChanged(this);

        } // OnSpeedChanged


        /// <summary>
        /// Raises the synchronized event.
        /// </summary>
        private void OnSynchronized()
        {
            if (Synchronized != null)
                Synchronized(this);

        } // OnSynchronized


        /// <summary>
        /// Raises the closing event.
        /// </summary>
        private void OnClosing()
        {
            if (Closing != null)
                Closing(this);

        } // OnCosing


        /// <summary>
        /// Raises the closed event.
        /// </summary>
        private void OnClosed()
        {
            if (Closed != null)
                Closed(this);

        } // OnClosed
        
    } // class FlowClient
} // namespace Lypshare.Flow